#include "gif.h"

#include <sys/eventfd.h>
#include <native_layer_jni.h>
#include <multimedia/image/image_pixel_map.h>
#ifdef __cplusplus
extern "C"{
#endif
typedef struct {
    struct pollfd eventPollFd;
    void *frameBuffer;
    uint8_t slurpHelper;
    pthread_mutex_t slurpMutex;
    pthread_cond_t slurpCond;
    uint8_t renderHelper;
    pthread_mutex_t renderMutex;
    pthread_cond_t renderCond;
    pthread_t slurpThread;
} SurfaceDescriptor;

static void *slurp(void *pVoidInfo) {
    GifInfo *info = (GifInfo *)pVoidInfo;
    SurfaceDescriptor *descriptor = (SurfaceDescriptor *)info->frameBufferDescriptor;
    while (1) {
        pthread_mutex_lock(&descriptor->slurpMutex);
        while (descriptor->slurpHelper == 0) {
            pthread_cond_wait(&descriptor->slurpCond, &descriptor->slurpMutex);
        }

        if (descriptor->slurpHelper == 2) {
            pthread_mutex_unlock(&descriptor->slurpMutex);
            DetachCurrentThread();
            return NULL;
        }
        descriptor->slurpHelper = 0;
        pthread_mutex_unlock(&descriptor->slurpMutex);
        DDGifSlurp(info, true, false);
        pthread_mutex_lock(&descriptor->renderMutex);
        descriptor->renderHelper = 1;
        pthread_cond_signal(&descriptor->renderCond);
        pthread_mutex_unlock(&descriptor->renderMutex);
    }
    return NULL;
}

static void releaseSurfaceDescriptor(GifInfo *info, JNIEnv *env) {
    SurfaceDescriptor *descriptor = (SurfaceDescriptor *)info->frameBufferDescriptor;
    info->frameBufferDescriptor = NULL;
    free(descriptor->frameBuffer);
    if (close(descriptor->eventPollFd.fd) != 0 && errno != EINTR) {
        throwException(env, RUNTIME_EXCEPTION_ERRNO, "Eventfd close failed ");
    }
    errno = pthread_mutex_destroy(&descriptor->slurpMutex);
    THROW_ON_NONZERO_RESULT(errno, "Slurp mutex destroy failed ");
    errno = pthread_mutex_destroy(&descriptor->renderMutex);
    THROW_ON_NONZERO_RESULT(errno, "Render mutex destroy failed ");
    errno = pthread_cond_destroy(&descriptor->slurpCond);
    THROW_ON_NONZERO_RESULT(errno, "Slurp cond destroy failed ");
    errno = pthread_cond_destroy(&descriptor->renderCond);
    THROW_ON_NONZERO_RESULT(errno, "Render cond  destroy failed ");
    free(descriptor);
}

JNIEXPORT void JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_bindSurface(JNIEnv *env, jclass handleClass, jlong gifInfo,
                                                    jobject jsurface, jlongArray savedState) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    SurfaceDescriptor *descriptor = (SurfaceDescriptor *)info->frameBufferDescriptor;
    if (descriptor == NULL) {
        descriptor = (SurfaceDescriptor *)malloc(sizeof(SurfaceDescriptor));
        if (descriptor == NULL) {
            throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
            return;
        }
        descriptor->eventPollFd.events = POLL_IN;
        descriptor->eventPollFd.fd = eventfd(0, 0);
        if (descriptor->eventPollFd.fd == -1) {
            throwException(env, RUNTIME_EXCEPTION_ERRNO, "Eventfd creation failed ");
            free(descriptor);
            return;
        }
        errno = pthread_cond_init(&descriptor->slurpCond, NULL);
        THROW_ON_NONZERO_RESULT(errno, "Slurp condition variable initialization failed ");
        errno = pthread_cond_init(&descriptor->renderCond, NULL);
        THROW_ON_NONZERO_RESULT(errno, "Render condition variable initialization failed ");
        errno = pthread_mutex_init(&descriptor->slurpMutex, NULL);
        THROW_ON_NONZERO_RESULT(errno, "Slurp mutex initialization failed ");
        errno = pthread_mutex_init(&descriptor->renderMutex, NULL);
        THROW_ON_NONZERO_RESULT(errno, "Render mutex initialization failed ");
        descriptor->frameBuffer = NULL;
        info->frameBufferDescriptor = descriptor;
        info->destructor = releaseSurfaceDescriptor;
    }

    eventfd_t eventValue;
    int pollResult;

    while (true) {
        pollResult = TEMP_FAILURE_RETRY(poll(&descriptor->eventPollFd, 1, 0));
        if (pollResult == 0)
            break;
        else if (pollResult > 0) {
            const int readResult = TEMP_FAILURE_RETRY(eventfd_read(descriptor->eventPollFd.fd, &eventValue));
            if (readResult != 0) {
                throwException(env, RUNTIME_EXCEPTION_ERRNO, "Could not read from eventfd ");
                return;
            }
        } else {
            throwException(env, RUNTIME_EXCEPTION_ERRNO, "Could not poll on eventfd ");
            return;
        }
    }
    const int32_t format = info->isOpaque ? OHOS_PIXEL_MAP_FORMAT_RGBA_8888 : OHOS_PIXEL_MAP_FORMAT_RGB_565;
    struct NativeLayer *layer = GetNativeLayer(env, jsurface);
    GifFileType *const gifFilePtr = info->gifFilePtr;
    if (NativeLayerHandle(layer, SET_WIDTH_AND_HEIGHT, (int32_t) gifFilePtr->SWidth, (int32_t) gifFilePtr->SHeight) != 0
                                  && NativeLayerHandle(layer, SET_FORMAT, format) != 0) {
        NativeLayerHandle(layer, RELEASE_REF);
        throwException(env, RUNTIME_EXCEPTION_ERRNO, "Buffers geometry setting failed ");
        return;
    }

    struct NativeLayerBuffer buffer = {.buffer_bits = NULL};
    void *oldBufferBits;

    if (NativeLayerHandle(layer, LOCK_LAYER) != 0) {
#ifdef DEBUG
        LOGE("Full window lock failed %d", errno);
#endif
    NativeLayerHandle(layer, RELEASE_REF);
        return;
    }
    const size_t bufferSize = buffer.buffer_steps * buffer.buffer_height * sizeof(argb);

    info->stride = (uint32_t) buffer.buffer_steps;
    long long invalidationDelayMillis;
    if (descriptor->frameBuffer) {
        memcpy(buffer.buffer_bits, descriptor->frameBuffer, bufferSize);
        invalidationDelayMillis = 0;
        descriptor->renderHelper = 1;
        descriptor->slurpHelper = 0;
    } else {
        if (savedState != NULL) {
            invalidationDelayMillis = restoreSavedState(info, env, savedState, buffer.buffer_bits);
            if (invalidationDelayMillis < 0) {
                invalidationDelayMillis = 0;
            }
        } else {
            invalidationDelayMillis = 0;
        }
        descriptor->renderHelper = 0;
        descriptor->slurpHelper = 1;
    }

    info->lastFrameRemainder = -1;
    NativeLayerHandle(layer, DRAW_LAYER);

    if (info->loopCount != 0 && info->currentLoop == info->loopCount) {
        NativeLayerHandle(layer, RELEASE_REF);
        pollResult = TEMP_FAILURE_RETRY(poll(&descriptor->eventPollFd, 1, -1));
        if (pollResult < 0) {
            throwException(env, RUNTIME_EXCEPTION_ERRNO, "Animation end poll failed ");
        }
        return;
    }

    errno = pthread_create(&descriptor->slurpThread, NULL, slurp, info);
    if (errno != 0) {
        throwException(env, RUNTIME_EXCEPTION_ERRNO, "Slurp thread creation failed ");
        NativeLayerHandle(layer, RELEASE_REF);
        return;
    }

    while (true) {
        pollResult = TEMP_FAILURE_RETRY(poll(&descriptor->eventPollFd, 1, (int) invalidationDelayMillis));
        long renderingStartTime = getRealTime();

        if (pollResult < 0) {
            throwException(env, RUNTIME_EXCEPTION_ERRNO, "Display loop poll failed ");
            break;
        } else if (pollResult > 0) {
            if (descriptor->frameBuffer == NULL) {
                descriptor->frameBuffer = malloc(bufferSize);
                if (descriptor->frameBuffer == NULL) {
                    throwException(env, OUT_OF_MEMORY_ERROR, OOME_MESSAGE);
                    break;
                }
            }
            memcpy(descriptor->frameBuffer, buffer.buffer_bits, bufferSize);
            break;
        }
        oldBufferBits = buffer.buffer_bits;

        const GifImageDesc imageDesc = gifFilePtr->SavedImages[info->currentIndex].ImageDesc;
        struct HArea dirtyRect;
        dirtyRect.posLeft = imageDesc.Left;
        dirtyRect.posTop = imageDesc.Top;
        dirtyRect.posRight = imageDesc.Left + imageDesc.Width;
        dirtyRect.posBottom = imageDesc.Top + imageDesc.Height;
        struct HArea *dirtyRectPtr = (info->currentIndex == 0) ? NULL : &dirtyRect;

        if (NativeLayerHandle(layer, LOCK_LAYER) != 0) {
#ifdef DEBUG
            LOGE("Partial window lock failed %d", errno);
#endif
            break;
        }

        if (info->currentIndex == 0)
            prepareCanvas((argb *)buffer.buffer_bits, info);
        else
            memcpy(buffer.buffer_bits, oldBufferBits, bufferSize);

        pthread_mutex_lock(&descriptor->renderMutex);
        while (descriptor->renderHelper == 0) {
            pthread_cond_wait(&descriptor->renderCond, &descriptor->renderMutex);
        }
        descriptor->renderHelper = 0;
        pthread_mutex_unlock(&descriptor->renderMutex);

        const uint_fast32_t frameDuration = getBitmap((argb*)buffer.buffer_bits, info);

        pthread_mutex_lock(&descriptor->slurpMutex);
        descriptor->slurpHelper = 1;
        pthread_cond_signal(&descriptor->slurpCond);
        pthread_mutex_unlock(&descriptor->slurpMutex);

        NativeLayerHandle(layer, DRAW_LAYER);
        invalidationDelayMillis = calculateInvalidationDelay(info, renderingStartTime, frameDuration);

        if (info->lastFrameRemainder >= 0) {
            invalidationDelayMillis = info->lastFrameRemainder;
            info->lastFrameRemainder = -1;
        }
    }

    NativeLayerHandle(layer, RELEASE_REF);
    pthread_mutex_lock(&descriptor->slurpMutex);
    descriptor->slurpHelper = 2;
    pthread_cond_signal(&descriptor->slurpCond);
    pthread_mutex_unlock(&descriptor->slurpMutex);
    errno = pthread_join(descriptor->slurpThread, NULL);
    THROW_ON_NONZERO_RESULT(errno, "Slurp thread join failed");
    return;
}

JNIEXPORT void JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_postUnbindSurface(JNIEnv *env, jclass handleClass, jlong gifInfo) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL || info->frameBufferDescriptor == NULL) {
        return;
    }
    SurfaceDescriptor const *descriptor = (SurfaceDescriptor *)info->frameBufferDescriptor;
    const int writeResult = TEMP_FAILURE_RETRY(eventfd_write(descriptor->eventPollFd.fd, 1));
    if (writeResult != 0 && errno != EBADF) {
        throwException(env, RUNTIME_EXCEPTION_ERRNO, "Could not write to eventfd ");
    }
}
#ifdef __cplusplus
}
#endif